package com.springboot.java_doc_searcher.searcher;

import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;

/**
 * Created with IntelliJ IDEA.
 * Description :
 * User: jiume
 * Date: 2025-06-07
 * Time: 19:20
 */
// 通过这个类实现用户输入查询词，展示相关页面的结果
public class DocSearcher {

    // 1、针对查询词进行分词处理
    // 2、将分词的结果依个去倒排索引中查询对应的文档Id列表（Weight类）
    // 3、将查询出的结果，按照相似性降序排序
    // 4、将排序后的结果，根据文档Id去正排索引中查询对应的文档信息，
    //    然后将title、URL、描述信息封装到Result对象中，并返回

    private final Index INDEX = new Index();

    private final Set<String> stopWord = new HashSet<>();

    private final String STOP_WORD = "D:/project/java-doc-searcher/index_path/stop_word.txt";

    // 实现搜索的时候，索引肯定得是加载好的，暂停词肯定也得是加载好的
    public DocSearcher() {
        INDEX.load();
        loadStopWord();
    }

    // 将文件中的暂停词加载到内存中
    private void loadStopWord() {
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(STOP_WORD))){
            while (true) {
                // 文件中每个单词是独占一行，因此每次读取一行，就是读取一个单词
                String line = bufferedReader.readLine();
                if (line == null) {
                    // 说明读到文件末尾了
                    break;
                }
                stopWord.add(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 实现整个搜索过程
     * @param query 查询词
     * @return 符合用户搜索结果的网页信息列表
     */
    public List<Result> search(String query) {
        // 1、对查询词分词处理
        List<Term> oldTerms = ToAnalysis.parse(query).getTerms();
        // 1.1 将分词后的结果，进行过滤（过滤暂停词）
        List<Term> terms = new ArrayList<>();
        for (Term term : oldTerms) {
            if (!stopWord.contains(term.getName())) {
                // 不包含，就加入到新集合中
                terms.add(term);
            }
        }
        // 2、根据分词结果去倒排索引中查询
        List<Weight> allInverted = new ArrayList<>();
        List<List<Weight>> oldInverted = new ArrayList<>();
        for (Term term : terms) {
            String name = term.getName(); // 分词结果
            // 这里得提前从本地磁盘的文件中读取去索引: 调用Index类的load()方法
            List<Weight> inverted = INDEX.getInverted(name);
            // 可能当前词并未在所有的文档中出现过，因此得判断当前列表是否为空
            if (inverted != null) {
                // 加入到二维列表中
                oldInverted.add(inverted);
            }
        }
        // 2.1 这里需要对文档Id进行去重处理
        allInverted = mergeResult(oldInverted);
        // 3、按照文档相关性，从强到弱排序
        // 使用集合自带的比较方法，并传入比较器参数
        allInverted.sort(new Comparator<Weight>() {
            @Override
            public int compare(Weight o1, Weight o2) {
                // 这里需要降序，即从大到小
                return o2.getWeight() - o1.getWeight();
            }
        });
        // 4、根据文档Id去正排索引中查询文档信息，使用Result对象封装
        List<Result> results = new ArrayList<>();
        for (Weight weight : allInverted) {
            DocInfo docInfo = INDEX.getDocInfo(weight.getDocId());
            Result result = new Result();
            result.setTitle(docInfo.getTitle());
            result.setUrl(docInfo.getUrl());
            // 通过正文获取描述信息
            result.setDescription(GetDescription(docInfo.getContent(), terms));
            results.add(result);
        }
        return results;
    }

    // 创建一个内部类表示某个文档的在二维列表中的具体位置
    static class Pos {
        int row;
        int col;

        public Pos(int row, int col) {
            this.row = row;
            this.col = col;
        }
    }

    // 对文档Id进行去重处理
    private List<Weight> mergeResult(List<List<Weight>> source) {
        // 1、先将二维列表中，每个一维列表针对文档Id升序排序
        for (List<Weight> weights : source) {
            weights.sort(new Comparator<Weight>() {
                @Override
                public int compare(Weight o1, Weight o2) {
                    // 升序排序: 从小到大
                    return o1.getDocId() - o2.getDocId();
                }
            });
        }
        // 2、使用 小根堆 来快速找到多路列表中的文档Id最小值
        // 指定 小根堆 的比较规则
        PriorityQueue<Pos> minHeap = new PriorityQueue<>(new Comparator<Pos>() {
            @Override
            public int compare(Pos o1, Pos o2) {
                // 先定位到对应的weight对象
                Weight w1 = source.get(o1.row).get(o1.col);
                Weight w2 = source.get(o2.row).get(o2.col);
                // 小根堆: 找到最小值
                return w1.getDocId() - w2.getDocId();
            }
        });
        // 3、将多路列表中每个列表的文档Id最小值的Pos对象加入小根堆中
        for (int i = 0; i < source.size(); i++) {
            minHeap.offer(new Pos(i, 0));
        }
        // 遍历小根堆，每次找出最小值，直至堆为空
        List<Weight> target = new ArrayList<>();
        while (!minHeap.isEmpty()) {
            Pos minPos = minHeap.poll();
            // 定位到对应的weight对象
            Weight weight = source.get(minPos.row).get(minPos.col);
            // 判断目标列表是否为空
            if (target.isEmpty()) {
                // 如果为空，直接加入即可
                target.add(weight);
            } else {
                // 如果不为空，判断当前weight对象的文档Id是否与目标列表中最后一个weight对象的文档Id相同
                Weight tmp = target.get(target.size() - 1);
                if (tmp.getDocId() == weight.getDocId()) {
                    // 相同，就合并权重（简单处理: 相加）
                    tmp.setWeight(tmp.getWeight() + weight.getWeight());
                } else {
                    // 不相同，就把当前weight对象添加到目标列表中
                    target.add(weight);
                }
            }
            // 将取出的pos位置所在列表往后+1位置的pos对象加入小根堆中
            // 这里不能盲目加入，得判断当前位置是否合法
            if (minPos.col+1 < source.get(minPos.row).size()) {
                minHeap.offer(new Pos(minPos.row, minPos.col+1));
            }
        }
        return target;
    }

    // 在正文中获取第一次出现的分词结果
    private String GetDescription(String content, List<Term> terms) {
        int firstPos = -1;
        for (Term term : terms) {
            String name = term.getName();
            // 由于分词后的结果都是小写字母，因此对于正文也得转小写才行
            // 其次这里匹配到的词得是 单独从全词才行，而不能是部分词
            firstPos = content.toLowerCase().indexOf(" "+name+" ");
            if (firstPos != -1) {
                // 说明正文中存在分词结果
                break;
            }
        }
        if (firstPos == -1) {
            // 说明正文中并不存在相关联的词，只是在标题中出现过而已
            if (content.length() > 160) {
                return content.substring(0, 160) + "...";
            }
            return content;
        }
        // 开始针对分词结果位置前后开始截取160个字符
        // 避免substring过程中，出现 左越界和右越界的情况
        int start = Math.max(0, firstPos-60);
        int end = Math.min(start+160, content.length());
        String tmp = null;
        if (end == content.length()) {
            tmp =  content.substring(start, end);
        } else {
            tmp = content.substring(start, end) + "...";
        }
        // 将tmp中出现的查询词标记，以便于前端处理标红
        for (Term term : terms) {
            String name = term.getName();
            tmp = tmp.replaceAll("(?i) " + name + " ", " <i>" + name + "</i> ");
        }
        return tmp;
    }

    // 验证搜索模块功能是否完整
    public static void main(String[] args) {
        DocSearcher docSearcher = new DocSearcher();
        System.out.print("请输入要查询的词: => ");
        Scanner scanner = new Scanner(System.in);
        String query = scanner.nextLine();
        List<Result> arrayList = docSearcher.search(query);
        for (Result result : arrayList) {
            System.out.println("====================");
            System.out.println(result);
        }
    }
}
